為了下一篇方便示範 Raymarching,這邊先來導入 fbx 與 mesh、material 相關的程式。這裡使用 assimp 來處理內容的讀取。
//mesh.h
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <DirectXMath.h>
#include "index_buffer.h"
#include "vertex_buffer.h"
class RenderContext;
struct MeshVertex
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT2 uv;
};
struct SMesh
{
VertexBuffer m_vertexBuffer;
std::vector<IndexBuffer> m_indexBuffers;
};
class Mesh
{
public:
void initFromFbxFile(const std::string& path);
void draw(RenderContext& renderContext) const;
private:
std::vector<std::unique_ptr<SMesh>> m_meshes;
};
//mesh.cpp
#include "mesh.h"
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include "render_context.h"
namespace
{
// 檢查 Buffer 元素數量是否能安全轉換成 int
int checkedBufferCount(std::size_t count, const char* bufferName)
{
// 確認元素數量沒有超過 int 可表示的最大值
if (count > static_cast<std::size_t>(std::numeric_limits<int>::max()))
{
// 超過支援範圍時拋出例外並指出是哪一種 Buffer
throw std::runtime_error(std::string("Mesh: ") + bufferName + " exceeds the supported size.");
}
// 將確認安全的元素數量轉換成 int
return static_cast<int>(count);
}
} // namespace
// 從指定的 FBX 檔案載入 Mesh 資料並建立 GPU Buffer
void Mesh::initFromFbxFile(const std::string& path)
{
// 檢查 FBX 檔案路徑是否為空
if (path.empty())
{
// 路徑為空時拋出參數錯誤
throw std::invalid_argument("Mesh: FBX path must not be empty.");
}
// 建立 Assimp Importer 負責讀取模型檔案
Assimp::Importer importer;
// 讀取 FBX 並進行三角化、轉換成 Left-Handed、產生法線與合併重複頂點
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_ConvertToLeftHanded |
aiProcess_GenNormals | aiProcess_JoinIdenticalVertices);
// 檢查模型是否載入失敗
if (scene == nullptr)
{
// 將 Assimp 提供的錯誤訊息一起拋出
throw std::runtime_error("Mesh: Failed to load FBX file '" + path + "': " + importer.GetErrorString());
}
// 檢查場景內是否至少存在一個 Mesh
if (!scene->HasMeshes())
{
throw std::runtime_error("Mesh: FBX file contains no meshes: " + path);
}
// 暫存這次成功載入的所有 Mesh
std::vector<std::unique_ptr<SMesh>> loadedMeshes;
// 預先配置足夠空間避免 vector 重複重新配置
loadedMeshes.reserve(scene->mNumMeshes);
// 逐一處理 Assimp 場景中的 Mesh
for (unsigned int meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
{
// 取得目前 Mesh 的參考
const aiMesh& sourceMesh = *scene->mMeshes[meshIndex];
// 確認 Mesh 同時具有頂點位置與法線資料
if (!sourceMesh.HasPositions() || !sourceMesh.HasNormals())
{
throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
" is missing required position or normal data.");
}
// 建立用來存放轉換後頂點資料的陣列
std::vector<MeshVertex> vertices;
// 預先配置與來源頂點數量相同的空間
vertices.reserve(sourceMesh.mNumVertices);
// 逐一轉換來源 Mesh 的頂點資料
for (unsigned int vertexIndex = 0; vertexIndex < sourceMesh.mNumVertices; ++vertexIndex)
{
// 取得目前頂點的位置
const aiVector3D& position = sourceMesh.mVertices[vertexIndex];
// 取得目前頂點的法線
const aiVector3D& normal = sourceMesh.mNormals[vertexIndex];
// 若存在第 0 組 UV 則讀取,否則使用零向量
const aiVector3D uv =
sourceMesh.HasTextureCoords(0) ? sourceMesh.mTextureCoords[0][vertexIndex] : aiVector3D{};
// 將位置、法線與 UV 轉換成引擎使用的 MeshVertex 格式
vertices.push_back({{position.x, position.y, position.z}, {normal.x, normal.y, normal.z}, {uv.x, uv.y}});
}
// 建立 32-bit Index 陣列
std::vector<std::uint32_t> indices;
// 每個三角形需要三個 Index,因此預先配置 Face 數量乘以三
indices.reserve(static_cast<std::size_t>(sourceMesh.mNumFaces) * 3);
// 逐一處理 Mesh 中的 Face
for (unsigned int faceIndex = 0; faceIndex < sourceMesh.mNumFaces; ++faceIndex)
{
// 取得目前 Face
const aiFace& face = sourceMesh.mFaces[faceIndex];
// 確認三角化後的 Face 確實只有三個 Index
if (face.mNumIndices != 3)
{
throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
" contains a non-triangle face after triangulation.");
}
// 將目前三角形的所有 Index 加入 Index 陣列
indices.insert(indices.end(), face.mIndices, face.mIndices + face.mNumIndices);
}
// 確認 Mesh 具有可供繪製的頂點與 Index
if (vertices.empty() || indices.empty())
{
throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
" contains no renderable geometry.");
}
// 建立新的 SMesh 並由 unique_ptr 管理生命週期
auto mesh = std::make_unique<SMesh>();
// 建立 Vertex Buffer 並設定頂點數量與單一頂點大小
mesh->m_vertexBuffer.init(checkedBufferCount(vertices.size(), "vertex count"),
static_cast<int>(sizeof(MeshVertex)));
// 將頂點資料複製到 Vertex Buffer
mesh->m_vertexBuffer.copy(vertices.data());
// 新增一個 Index Buffer
mesh->m_indexBuffers.emplace_back();
// 取得剛新增的 Index Buffer
IndexBuffer& indexBuffer = mesh->m_indexBuffers.back();
// 建立 Index Buffer 並設定 Index 數量與單一 Index 大小
indexBuffer.init(checkedBufferCount(indices.size(), "index count"), static_cast<int>(sizeof(std::uint32_t)));
// 將 Index 資料複製到 Index Buffer
indexBuffer.copy(indices.data());
// 將完成初始化的 Mesh 移入暫存陣列
loadedMeshes.push_back(std::move(mesh));
}
// 所有 Mesh 都成功載入後再一次替換目前持有的 Mesh
m_meshes = std::move(loadedMeshes);
}
// 繪製目前 Mesh 物件持有的所有幾何資料
void Mesh::draw(RenderContext& renderContext) const
{
// 逐一處理所有子 Mesh
for (const auto& mesh : m_meshes)
{
// 將目前 Mesh 的 Vertex Buffer 綁定到 Render Pipeline
renderContext.setVertexBuffer(mesh->m_vertexBuffer);
// 逐一處理目前 Mesh 的所有 Index Buffer
for (const IndexBuffer& indexBuffer : mesh->m_indexBuffers)
{
// 綁定目前的 Index Buffer
renderContext.setIndexBuffer(indexBuffer);
// 根據 Index 數量送出 Indexed Draw Call
renderContext.drawIndexed(indexBuffer.getCount());
}
}
}
//material.h
#pragma once
#include <DirectXMath.h>
#include "constant_buffer.h"
struct MaterialConstants
{
DirectX::XMFLOAT4 baseColor = {1.0f, 1.0f, 1.0f, 1.0f};
DirectX::XMFLOAT4 emissiveAndStrength = {0.0f, 0.0f, 0.0f, 0.0f};
DirectX::XMFLOAT4 surface = {0.5f, 0.0f, 1.0f, 0.0f};
};
static_assert(sizeof(MaterialConstants) % 16 == 0);
class Material
{
public:
void init(const MaterialConstants& constants = {});
void setConstants(const MaterialConstants& constants);
D3D12_GPU_VIRTUAL_ADDRESS getConstantsAddress() const;
private:
ConstantBuffer m_constantBuffer;
MaterialConstants m_constants{};
};
//material.cpp
#include "material.h"
// 使用材質常數初始化 Material 與對應的 Constant Buffer
void Material::init(const MaterialConstants& constants)
{
// 保存目前材質使用的常數資料
m_constants = constants;
// 建立 Constant Buffer 並寫入初始材質常數
m_constantBuffer.init(sizeof(MaterialConstants), &m_constants);
}
// 更新材質常數並同步到 GPU
void Material::setConstants(const MaterialConstants& constants)
{
// 更新 CPU 端保存的材質常數
m_constants = constants;
// 將最新的材質常數複製到 VRAM
m_constantBuffer.copyToVRAM(m_constants);
}
// 取得材質 Constant Buffer 的 GPU Virtual Address
D3D12_GPU_VIRTUAL_ADDRESS Material::getConstantsAddress() const
{
// 回傳 Constant Buffer 在 GPU 上的虛擬位址
return m_constantBuffer.getGPUVirtualAddress();
}
///main.cpp
#include <cstdlib>
#include <cstring>
#include <exception>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <DirectXMath.h>
#include <directx/d3dx12_core.h>
#include <wrl/client.h>
#include "graphics_engine.h"
#include "mesh.h"
#include "my_engine.h"
#include "pipeline_state.h"
#include "render_context.h"
#include "shader.h"
#include "system.h"
namespace
{
using Microsoft::WRL::ComPtr;
struct TransformConstants
{
DirectX::XMFLOAT4X4 worldViewProjection;
};
// 取得執行檔所在目錄,方便從相對路徑尋找 FBX 資源
std::filesystem::path getExecutableDirectory()
{
wchar_t executablePath[MAX_PATH]{};
const DWORD pathLength = GetModuleFileNameW(nullptr, executablePath, _countof(executablePath));
if (pathLength == 0 || pathLength == _countof(executablePath))
throw std::runtime_error("DayX_ReadFBX: Failed to determine the executable directory.");
return std::filesystem::path(executablePath).parent_path();
}
ComPtr<ID3D12RootSignature> createRootSignature(ID3D12Device* device)
{
if (device == nullptr)
throw std::invalid_argument("DayX_ReadFBX: A graphics device is required.");
D3D12_ROOT_PARAMETER transformParameter{};
transformParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
transformParameter.Descriptor.ShaderRegister = 0;
transformParameter.Descriptor.RegisterSpace = 0;
transformParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
D3D12_ROOT_SIGNATURE_DESC description{};
description.NumParameters = 1;
description.pParameters = &transformParameter;
description.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ComPtr<ID3DBlob> serializedRootSignature;
ComPtr<ID3DBlob> errorBlob;
const HRESULT serializeResult = D3D12SerializeRootSignature(
&description, D3D_ROOT_SIGNATURE_VERSION_1, serializedRootSignature.GetAddressOf(), errorBlob.GetAddressOf());
if (FAILED(serializeResult))
{
std::string message = "DayX_ReadFBX: Failed to serialize the root signature.";
if (errorBlob != nullptr)
{
message += "\n";
message.append(static_cast<const char*>(errorBlob->GetBufferPointer()), errorBlob->GetBufferSize());
}
throw std::runtime_error(message);
}
ComPtr<ID3D12RootSignature> rootSignature;
if (FAILED(device->CreateRootSignature(0, serializedRootSignature->GetBufferPointer(),
serializedRootSignature->GetBufferSize(),
IID_PPV_ARGS(rootSignature.GetAddressOf()))))
{
throw std::runtime_error("DayX_ReadFBX: Failed to create the root signature.");
}
return rootSignature;
}
PipelineState createPipelineState(ID3D12RootSignature* rootSignature, Shader& vertexShader, Shader& pixelShader)
{
if (rootSignature == nullptr)
throw std::invalid_argument("DayX_ReadFBX: A root signature is required.");
// 定義 FBX 頂點資料中的 Position、Normal 與 UV Layout
static constexpr D3D12_INPUT_ELEMENT_DESC inputElementDescriptions[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
};
D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};
description.InputLayout = {inputElementDescriptions, _countof(inputElementDescriptions)};
description.pRootSignature = rootSignature;
description.VS = CD3DX12_SHADER_BYTECODE(vertexShader.getCompiledBlob());
description.PS = CD3DX12_SHADER_BYTECODE(pixelShader.getCompiledBlob());
description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
description.DepthStencilState.DepthEnable = FALSE;
description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
description.DepthStencilState.StencilEnable = FALSE;
description.SampleMask = UINT_MAX;
description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
description.NumRenderTargets = 1;
description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
description.SampleDesc.Count = 1;
PipelineState pipelineState;
pipelineState.init(description);
return pipelineState;
}
ComPtr<ID3D12Resource> createTransformBuffer(ID3D12Device* device, const TransformConstants& constants)
{
if (device == nullptr)
throw std::invalid_argument("DayX_ReadFBX: A graphics device is required.");
constexpr UINT64 allocationSize = D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT;
const D3D12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
const D3D12_RESOURCE_DESC resourceDescription = CD3DX12_RESOURCE_DESC::Buffer(allocationSize);
ComPtr<ID3D12Resource> transformBuffer;
if (FAILED(device->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &resourceDescription,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
IID_PPV_ARGS(transformBuffer.GetAddressOf()))))
{
throw std::runtime_error("DayX_ReadFBX: Failed to create the transform constant buffer.");
}
void* mappedData = nullptr;
const D3D12_RANGE readRange{0, 0};
if (FAILED(transformBuffer->Map(0, &readRange, &mappedData)))
throw std::runtime_error("DayX_ReadFBX: Failed to map the transform constant buffer.");
std::memcpy(mappedData, &constants, sizeof(constants));
transformBuffer->Unmap(0, nullptr);
return transformBuffer;
}
TransformConstants createTransformConstants()
{
using namespace DirectX;
const XMMATRIX world = XMMatrixIdentity();
const XMVECTOR eyePosition = XMVectorSet(0.0f, 2.25f, -7.5f, 1.0f);
const XMVECTOR focusPosition = XMVectorZero();
const XMVECTOR upDirection = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
const XMMATRIX view = XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);
const XMMATRIX projection =
XMMatrixPerspectiveFovLH(XMConvertToRadians(60.0f),
static_cast<float>(FRAME_BUFFER_W) / static_cast<float>(FRAME_BUFFER_H), 0.1f, 100.0f);
TransformConstants constants{};
XMStoreFloat4x4(&constants.worldViewProjection, world * view * projection);
return constants;
}
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
try
{
initWindow(hInstance, hPrevInstance, lpCmdLine, nCmdShow, TEXT("DayX Read FBX"));
if (g_hWnd == nullptr)
throw std::runtime_error("DayX_ReadFBX: Failed to create the application window.");
GraphicsEngine graphicsEngine;
graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);
ID3D12Device* device = graphicsEngine.getD3DDevice();
const ComPtr<ID3D12RootSignature> rootSignature = createRootSignature(device);
// 載入用來繪製 FBX Mesh 的 Vertex Shader 與 Pixel Shader
Shader vertexShader;
Shader pixelShader;
vertexShader.loadVS("assets/shaders/fbx.hlsl", "VSMain");
pixelShader.loadPS("assets/shaders/fbx.hlsl", "PSMain");
// 建立符合 FBX 頂點格式的 Pipeline State
PipelineState pipelineState = createPipelineState(rootSignature.Get(), vertexShader, pixelShader);
const TransformConstants transformConstants = createTransformConstants();
const ComPtr<ID3D12Resource> transformBuffer = createTransformBuffer(device, transformConstants);
// 建立用來保存 FBX 模型資料的 Mesh
Mesh cube;
// 組合 Cube.fbx 的完整檔案路徑
const std::filesystem::path cubePath = getExecutableDirectory() / "assets" / "fbx" / "Cube.fbx";
// 讀取 FBX 並建立對應的 Vertex Buffer 與 Index Buffer
cube.initFromFbxFile(cubePath.string());
RenderContext& renderContext = graphicsEngine.getRenderContext();
while (dispatchWindowMessage())
{
graphicsEngine.beginRender();
renderContext.setRootSignature(rootSignature.Get());
// 套用能處理 FBX 頂點格式的 Pipeline State
renderContext.setPipelineState(pipelineState);
// FBX 載入時已三角化,因此以 Triangle List 方式繪製
renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
renderContext.setGraphicsRootConstantBufferView(0, transformBuffer->GetGPUVirtualAddress());
// 繪製從 Cube.fbx 載入的所有 Mesh
cube.draw(renderContext);
graphicsEngine.endRender();
}
return EXIT_SUCCESS;
}
catch (const std::exception& exception)
{
MessageBoxA(nullptr, exception.what(), "DayX_ReadFBX initialization failed", MB_OK | MB_ICONERROR);
return EXIT_FAILURE;
}
catch (...)
{
MessageBoxA(nullptr, "An unknown fatal error occurred.", "DayX_ReadFBX initialization failed",
MB_OK | MB_ICONERROR);
return EXIT_FAILURE;
}
}

https://github.com/assimp/assimp